
package org.nViron.Render;

import com.sun.opengl.util.GLUT;
import com.sun.opengl.util.j2d.TextRenderer;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;
import java.awt.Font;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.glu.GLU;
import javax.media.opengl.glu.GLUquadric;
import org.nViron.nvPlaces.nvPlace;
import org.nViron.nvTraces.nvTrace;
import org.nViron.Client.Config;
import org.nViron.nvProjection.nvMercator;
import org.nViron.nvCommon.nvPosition;
import org.nViron.nvOrganism.nvStateMgr;
import org.nViron.nvData.nvDataEntity;
import org.nViron.nvOrganic.nvLeaf;
import org.nViron.nvOrganic.nvNode;
import org.nViron.nvOrganic.nvOrganic;
import org.nViron.nvOrganic.nvRoot;
import org.nViron.nvOrganic.nvStalk;
import org.nViron.nvOrganic.nvStem;
import org.nViron.nvParams.nvVizParamMgr.VIZPARAM;
import org.nViron.nvRender.nvFrustum;
import org.nViron.nvRender.nvRenderer;
import org.nViron.nvRender.nvVec3;
import org.nViron.nvSim.nvSimulator;

/**
 * OpenGL 1.2 implementation of nvRenderer based on JOGL
 * @author Roland Haring <p>
 * <script type="text/javascript" src="http://www.ohloh.net/p/376434/widgets/project_basic_stats.js"></script>
 */
public class RendererGL extends nvRenderer {

    private static final int SHADOW_RENDERPASS = 0;
    private static final int NORMAL_RENDERPASS = 1;
    private static final int TOTAL_RENDERPASS = 2;

    private TextRenderer textrenderer2d;
    private TextRenderer textrenderer3d;
    private GLAutoDrawable drawable;
    private GLU glu = new GLU();
    private GLUT glut = new GLUT();
    private GLUquadric gluQuadric = glu.gluNewQuadric();
    private Texture backgroundTex;
    private long timestamp = System.currentTimeMillis();
    private int framecount;
    private float fps;
    private GL gl;
    private int sphereList1 = -1;
    private int sphereList2 = -1;
    private int sphereList3 = -1;
    private int sphereList4 = -1;
    private int rootList = -1;
    private float organicsScale = 1.0f; // does not work yet!!
    private float[] shadowGroundplane = new float[4]; // a plane with equation ax + by + cz + d = 0
    private float[] shadowLightPosition = new float[4];
    private float[] shadowTransMat = new float[16];
    private boolean useDisplayList = false;
    private int renderpass;
    private boolean renderShadow = false;

    public RendererGL(GLAutoDrawable _drawable, nvSimulator _sim) {
        super(_sim);
        drawable = _drawable;
        setRenderInfo(true);
    }

    public GLAutoDrawable getDrawable() {
        return drawable;
    }

    public void setDrawable(GLAutoDrawable drawable) {
        this.drawable = drawable;
    }

    public FrustumGL getFrustumGL() {
        return (FrustumGL) frustum;
    }

    public GLU getGlu() {
        return glu;
    }

    public GLUquadric getGluQuadric() {
        return gluQuadric;
    }

    public GLUT getGlut() {
        return glut;
    }

    public TextRenderer getTextrenderer() {
        return textrenderer2d;
    }

    @Override
    public void init() {
        super.init();
        gl = drawable.getGL();
        
        textrenderer2d = new TextRenderer(new Font("Calibri", Font.PLAIN, 12));
        textrenderer2d.setSmoothing(true);
        textrenderer3d = new TextRenderer(new Font("Calibri", Font.PLAIN, 128));
        textrenderer3d.setSmoothing(true);

        Config config = (Config) simulator.getConfig();
        File texFile = new File(config.getTexturePath() + "background1024.png");
        try {
            backgroundTex = TextureIO.newTexture(texFile, false);
        } catch (IOException ex) {
            Logger.getLogger(RendererGL.class.getName()).log(Level.SEVERE, null, ex);
        }

        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA);
        gl.glEnable(gl.GL_COLOR_MATERIAL);
        gl.glShadeModel(gl.GL_SMOOTH);
        gl.glEnable(gl.GL_CULL_FACE);
        gl.glEnable(gl.GL_DEPTH_TEST);
        gl.glEnable(gl.GL_BLEND);

        setupLight(new nvPosition(simulator.getCameraPosition().getLongitude(), simulator.getCameraPosition().getLatitude(), 500.0f));
        float y = 0.001f;
        setShadowFloorPlane(new nvVec3(1, y, 0), new nvVec3(-1, y, 0), new nvVec3(0, y, 1));
    }

    @Override
    protected void setupCamera() {
        nvMercator mercator = (nvMercator) simulator.getProjection();
        mercator.setCameraPos(simulator.getCameraPosition());

        gl.glMatrixMode(gl.GL_MODELVIEW);
        gl.glLoadIdentity();

        nvVec3 p = mercator.geoToViz(simulator.getCameraPosition());
        nvVec3 l = mercator.geoToViz(simulator.getLookAtPosition());
        nvVec3 u = new nvVec3(0, 1, 0);
        glu.gluLookAt(p.x, p.y, p.z, l.x, l.y, l.z, u.x, u.y, u.z);
        frustum.setCamera(p, l, u);
    }

    @Override
    protected void renderForeground() {
        renderOverlayText();
    }

    @Override
    protected void renderBackground() {
        disableLight();
        disableFog();
        gl.glEnable(gl.GL_BLEND);

        gl.glMatrixMode(gl.GL_PROJECTION);
        gl.glPushMatrix();
        gl.glLoadIdentity();
        gl.glMatrixMode(gl.GL_MODELVIEW);
        gl.glPushMatrix();
        gl.glLoadIdentity();

        gl.glDisable(gl.GL_DEPTH_TEST);
        gl.glDepthMask(false);

        backgroundTex.bind();
        backgroundTex.enable();
        gl.glEnable(gl.GL_BLEND);
        gl.glBegin(gl.GL_QUADS);
        gl.glColor4f(1, 1, 1, 1.0f);
        gl.glTexCoord2f(0, 0);
        gl.glVertex2f(-1, -1);
        gl.glTexCoord2f(1, 0);
        gl.glVertex2f(1, -1);
        gl.glTexCoord2f(1, 1);
        gl.glVertex2f(1, 1);
        gl.glTexCoord2f(0, 1);
        gl.glVertex2f(-1, 1);
        gl.glEnd();
        backgroundTex.disable();

        gl.glDepthMask(true);
        gl.glEnable(gl.GL_DEPTH_TEST);

        gl.glMatrixMode(gl.GL_PROJECTION);
        gl.glPopMatrix();
        gl.glMatrixMode(gl.GL_MODELVIEW);
        gl.glPopMatrix();
    }

    @Override
    protected void renderMap() {
        enableFog();
        disableLight();

        if (simulator.getMapSource() != null && simulator.getProjection() != null && frustum.doesIntersectGround()) {

            nvVec3 ulVec = new nvVec3(Math.min(frustum.gnl.x, Math.min(frustum.gnr.x, Math.min(frustum.gfr.x, frustum.gfl.x))),
                    0.0f,
                    Math.min(frustum.gnl.z, Math.min(frustum.gnr.z, Math.min(frustum.gfr.z, frustum.gfl.z))));
            nvVec3 lrVec = new nvVec3(Math.max(frustum.gnl.x, Math.max(frustum.gnr.x, Math.max(frustum.gfr.x, frustum.gfl.x))),
                    0.0f,
                    Math.max(frustum.gnl.z, Math.max(frustum.gnr.z, Math.max(frustum.gfr.z, frustum.gfl.z))));

            nvPosition ul = simulator.getProjection().vizToGeo(ulVec);
            nvPosition lr = simulator.getProjection().vizToGeo(lrVec);

            simulator.getMapSource().setAreaOfInterest(ul, lr);
            simulator.getMapSource().render();
        }
    }

    @Override
    protected void renderGrid() {
        if (!frustum.doesIntersectGround()) {
            return;
        }

        disableFog();
        disableLight();
        gl.glDisable(gl.GL_DEPTH_TEST);

        // render frustum ground intersection
        gl.glColor4f(0.75f, 0.75f, 0.0f, 0.75f);
        gl.glBegin(gl.GL_LINES);
        gl.glVertex3f(frustum.gnl.x, 0, frustum.gnl.z);
        gl.glVertex3f(frustum.gnr.x, 0, frustum.gnr.z);
        gl.glVertex3f(frustum.gfr.x, 0, frustum.gfr.z);
        gl.glVertex3f(frustum.gfl.x, 0, frustum.gfl.z);
        gl.glEnd();

        // render lines on frustum ground intersection
        gl.glColor4f(0.75f, 0.75f, 0.75f, 0.25f);
        gl.glBegin(gl.GL_LINES);

        nvVec3 min = new nvVec3(Math.min(frustum.gnl.x, Math.min(frustum.gnr.x, Math.min(frustum.gfr.x, frustum.gfl.x))),
                0.0f,
                Math.min(frustum.gnl.z, Math.min(frustum.gnr.z, Math.min(frustum.gfr.z, frustum.gfl.z))));
        nvVec3 max = new nvVec3(Math.max(frustum.gnl.x, Math.max(frustum.gnr.x, Math.max(frustum.gfr.x, frustum.gfl.x))),
                0.0f,
                Math.max(frustum.gnl.z, Math.max(frustum.gnr.z, Math.max(frustum.gfr.z, frustum.gfl.z))));

        float steps = 25.0f;
        float deltaX = (max.x - min.x) / steps;
        for (int x = 0; x <= steps; x++) {
            gl.glVertex3f(min.x + deltaX * x, 0, min.z);
            gl.glVertex3f(min.x + deltaX * x, 0, max.z);
        }

        float deltaZ = (max.z - min.z) / steps;
        for (int z = 0; z < steps; z++) {
            gl.glVertex3f(min.x, 0, min.z + deltaZ * z);
            gl.glVertex3f(max.x, 0, min.z + deltaZ * z);
        }
        gl.glEnd();
    }

    @Override
    public void onReshape(int x, int y, int width, int height) {
        if (height <= 0) { // avoid a divide by zero error!
            height = 1;
        }
        float aspect = (float) width / (float) height;
        frustum.setProjection(frustum.getFOV(), aspect, frustum.getNearP(), frustum.getFarP());
        glu.gluPerspective(frustum.getFOV(), frustum.getAspect(), frustum.getNearP(), frustum.getFarP());
    }

    @Override
    protected int getNumRenderPasses() {
        return (renderShadow ? TOTAL_RENDERPASS : 1);
    }

    @Override
    protected void onPreRenderScene(int renderpass) {
        if (renderShadow) {
            this.renderpass = renderpass;
            beginStencilShadow();
        } else {
            this.renderpass = NORMAL_RENDERPASS;
        }
    }

    @Override
    protected void onPostRenderScene(int renderpass) {
        if (renderShadow) {
            endStencilShadow(0.4f, 0.0f, 0.0f, 0.0f);
        }
    }

    private void enableFog() {

        float density = 2.0f;
        // float fogColor[] = {0.1f, 0.1f, 0.1f, 1.0f};
        //float fogColor[] = {0.404f, 0.549f, 0.655f, 1.0f};
        float fogColor[] = {0.9f, 0.95f, 1.0f, 1.0f};
        gl.glEnable(gl.GL_FOG);
        gl.glFogi(gl.GL_FOG_MODE, gl.GL_LINEAR);
        gl.glFogfv(gl.GL_FOG_COLOR, fogColor, 0);
        gl.glFogf(gl.GL_FOG_DENSITY, density);
        gl.glFogf(gl.GL_FOG_START, frustum.getFarP() / 2.0f);
        gl.glFogf(gl.GL_FOG_END, frustum.getFarP());
        gl.glHint(gl.GL_FOG_HINT, gl.GL_NICEST);
    }

    private void disableFog() {
        gl.glDisable(gl.GL_FOG);
    }

    private void setupLight(nvPosition pos) {
        nvVec3 vec = simulator.getProjection().geoToViz(pos);
        vec.x = 0; vec.y = 1; vec.z = 0;
        float lightPos[] = {vec.x, vec.y, vec.z, 0};
        float ambient[] = {0.3f, 0.3f, 0.3f, 1.0f};
        float diffuse[] = {1.0f, 1.0f, 1.0f, 1.0f};
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, lightPos, 0);
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_AMBIENT, ambient, 0);
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_DIFFUSE, diffuse, 0);
        setShadowLightPos(lightPos);
    }

    private void disableLight() {
        gl.glDisable(gl.GL_LIGHTING);
        gl.glDisable(gl.GL_LIGHT0);
    }

    private void enableLight() {
        gl.glEnable(gl.GL_LIGHTING);
        gl.glEnable(gl.GL_LIGHT0);
    }

    @Override
    protected nvFrustum createFrustum() {
        return new FrustumGL(drawable);
    }

    private void renderOverlayText() {
        textrenderer2d.beginRendering(drawable.getWidth(), drawable.getHeight(), true);
        textrenderer2d.setColor(1.0f, 1.0f, 1.0f, 1.0f);
        gl.glDisable(gl.GL_DEPTH_TEST);
        if (isRenderInfo()) {
            framecount++;
            long diff = System.currentTimeMillis() - timestamp;
            if (diff > 1000) {
                timestamp += diff;
                fps = 1000.0f * (float) framecount / (float) diff;
                framecount = 0;
            }
            textrenderer2d.draw("FPS " + fps, 10, 10);

            double lineHeight = textrenderer2d.getBounds("A").getHeight(); // approximation
            int linePosY = 10;
            int linePosX = drawable.getWidth() - 150;
            for (int i = 4; i >= 0; i--) {
                textrenderer2d.draw(nvStateMgr.getIdentifier(i) + " : " + nvStateMgr.getCount(i), linePosX, linePosY);
                linePosY += lineHeight * 1.2f;
            }
            textrenderer2d.draw("Life State Distribution:", linePosX, linePosY + (int) lineHeight);
        }
        gl.glEnable(gl.GL_DEPTH_TEST);
        textrenderer2d.endRendering();
    }

    @Override
    public void renderOrganic(nvOrganic organic) {
        gl.glPushMatrix();
         nvVec3 pos = organic.getCurrentRenderPos();
        // some of the translation is done in each tissues render call. we need to componsate this here and translate to the required position
        gl.glTranslatef(pos.x - pos.x*organicsScale, pos.y - pos.y*organicsScale, pos.z - pos.z*organicsScale);
        gl.glScalef(organicsScale, organicsScale, organicsScale);
        organic.setScale(organicsScale);
        // organic is rendered by its parts which are called from organicmgr
    }

    @Override
    public void postRenderOrganic(nvOrganic organic) {
        gl.glPopMatrix();
    }

    @Override
    public void renderPlace(nvPlace place) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        enableLight();
        enableFog();

        if (isRenderDebugInfo()) {
            nvVec3 vec1 = simulator.getProjection().geoToViz(place.getCenter());
            gl.glColor4f(1.0f, 0.0f, 0.0f, 0.5f);
            gl.glBegin(gl.GL_LINES);
            synchronized (place.getEntities()) {
                for (Iterator it = place.getEntities().iterator(); it.hasNext();) {
                    nvDataEntity entity = (nvDataEntity) it.next();
                    nvVec3 vec2 = simulator.getProjection().geoToViz(entity.getPosition());
                    gl.glVertex3f(vec1.x, vec1.y, vec1.z);
                    gl.glVertex3f(vec2.x, vec2.y, vec2.z);
                }
            }
            gl.glEnd();
        }

        // TODO implement
        // reference? http://www.cartogrammar.com/blog/flickr-as-a-paintbrush/

        disableLight();
        disableFog();
    }

    @Override
    public void renderTrace(nvTrace trace) {
        // TODO implement
    }

    @Override
    public void renderRoot(nvRoot root) {
        if (renderpass != SHADOW_RENDERPASS) {
            gl.glEnable(gl.GL_BLEND);
            gl.glEnable(gl.GL_DEPTH_TEST);
            float radius = simulator.getProjection().geoToVizApprox(5.0f);
            nvVec3 pos = simulator.getProjection().geoToViz(root.getOrganic().getPosition());
            if (frustum.sphereInFrustum(new nvVec3(pos.x, 0, pos.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
                gl.glPushMatrix();
                gl.glTranslatef(pos.x, 0, pos.z);

                if (rootList == -1 || !useDisplayList) {
                    if (useDisplayList) {
                        rootList = gl.glGenLists(1);
                        gl.glNewList(rootList, gl.GL_COMPILE_AND_EXECUTE);
                    }
                    disableLight();
                    gl.glPushMatrix();
                    gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
                    gl.glTranslatef(0, 0.001f, 0);
                    gl.glRotatef(-90.0f, 1, 0, 0);
                    glu.gluDisk(gluQuadric, 0, radius * 3.0f, 12, 1);
                    gl.glPopMatrix();

                    gl.glTranslatef(0, 0.001f, 0);
                    gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
                    gl.glRotatef(-90.0f, 1, 0, 0);
                    glu.gluDisk(gluQuadric, 0, radius, 12, 1);
                    if (useDisplayList) {
                        gl.glEndList();
                    }
                } else {
                    gl.glCallList(rootList);
                }
                gl.glPopMatrix();
            }
        }
    }

    @Override
    public void renderStem(nvStem stem) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        disableLight();

        float radius = simulator.getProjection().geoToVizApprox(0.5f);
        nvVec3 pos1 = stem.getSource().getCurrentRenderPos();
        nvVec3 pos2 = stem.getDestination().getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos1.x, pos1.y, pos1.z), radius) != nvFrustum.ALIGN.OUTSIDE ||
                frustum.sphereInFrustum(new nvVec3(pos2.x, pos2.y, pos2.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
            float width = 2.0f + stem.getNumEntities() / 8.0f;
            if (width > 8.0f) {
                width = 8.0f;
            }
            gl.glLineWidth(width);
            //gl.glColor4f(24.0f/255.0f, 128.0f/255.0f, 203.0f/255.0f, 1.0f);
            gl.glColor4f(0.0f, 0.0f, 0.0f, 0.75f);
            gl.glBegin(gl.GL_LINES);
            gl.glVertex3f(pos1.x, pos1.y, pos1.z);
            gl.glVertex3f(pos2.x, pos2.y, pos2.z);
            gl.glEnd();
            gl.glLineWidth(1.0f);
        }
    }

    private void renderBillboardedText(String text, nvVec3 pos, boolean centered) {
        gl.glPushMatrix();
        Rectangle2D bounds = textrenderer3d.getBounds(text);
        float scale = 0.000075f;
        float xtrans = 0.0f;
        float ytrans = 0.0f;
        if (centered) {
            xtrans = -(float) bounds.getCenterX() * scale;
            ytrans = (float) bounds.getCenterY() * scale;
        }
        nvVec3 textPos = new nvVec3(pos.x + xtrans, pos.y + ytrans, pos.z);
        gl.glTranslatef(textPos.x, textPos.y, textPos.z);
        billboardCylindrical(simulator.getProjection().geoToViz(simulator.getCameraPosition()), textPos);
        textrenderer3d.setColor(1, 1, 1, 1);
        textrenderer3d.begin3DRendering();
        textrenderer3d.draw3D(text, 0, 0, 0, scale);
        textrenderer3d.end3DRendering();
        gl.glPopMatrix();
    }

    /*-----------------------------------------------------------------
     * The objects motion is restricted to a rotation on a predefined axis
     * The function bellow does cylindrical billboarding on the Y axis, i.e.
     * the object will be able to rotate on the Y axis only.
     * code based on http://www.lighthouse3d.com/opengl/billboarding/index.php3?billSource
    -----------------------------------------------------------------*/
    private void billboardCylindrical(nvVec3 cam, nvVec3 worldPos) {
        nvVec3 lookAt = new nvVec3(0, 0, 1);
        nvVec3 objToCamProj = new nvVec3();
        nvVec3 upAux;
        float angleCosine;

        // objToCamProj is the vector in world coordinates from the local origin to the camera
        // projected in the XZ plane
        objToCamProj.x = cam.x - worldPos.x;
        objToCamProj.y = 0;
        objToCamProj.z = cam.z - worldPos.z;

        // normalize both vectors to get the cosine directly afterwards
        objToCamProj.normalize();

        // easy fix to determine wether the angle is negative or positive
        // for positive angles upAux will be a vector pointing in the
        // positive y direction, otherwise upAux will point downwards
        // effectively reversing the rotation.
        upAux = lookAt.cross(objToCamProj);

        // compute the angle
        angleCosine = lookAt.dot(objToCamProj);

        // perform the rotation. The if statement is used for stability reasons
        // if the lookAt and v vectors are too close together then |aux| could
        // be bigger than 1 due to lack of precision
        if ((angleCosine < 0.99990) && (angleCosine > -0.9999)) {
            gl.glRotatef((float) Math.acos(angleCosine) * 180.0f / (float) Math.PI, upAux.x, upAux.y, upAux.z);
        }
    }

    @Override
    public void renderNode(nvNode node) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        float radius = simulator.getProjection().geoToVizApprox(2.5f);
        nvVec3 pos = node.getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos.x, pos.y, pos.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
            enableLight();
            gl.glPushMatrix();
            gl.glTranslatef(pos.x, pos.y, pos.z);
            float scale = 1.0f + node.getNumEntities() / 25.0f;
            if (scale > 4.0f) {
                scale = 4.0f;
            }
            gl.glScalef(scale, scale, scale);

            if (sphereList1 == -1 || !useDisplayList) {
                if (useDisplayList) {
                    sphereList1 = gl.glGenLists(1);
                    gl.glNewList(sphereList1, gl.GL_COMPILE_AND_EXECUTE);
                }
                gl.glCullFace(gl.GL_FRONT);
                gl.glPushMatrix();
                gl.glDepthMask(false);
                enableLight();
                gl.glScalef(1.75f, 1.75f, 1.75f);
                gl.glColor4f(28.0f / 255.0f, 92.0f / 255.0f, 139.0f / 255.0f, 1.0f);
                glu.gluSphere(gluQuadric, radius, 15, 15);
                gl.glCullFace(gl.GL_BACK);
                gl.glDepthMask(true);
                gl.glPopMatrix();
                if (useDisplayList) {
                    gl.glEndList();
                }
            } else {
                gl.glCallList(sphereList1);
            }

            if (sphereList2 == -1 || !useDisplayList) {
                if (useDisplayList) {
                    sphereList2 = gl.glGenLists(1);
                    gl.glNewList(sphereList2, gl.GL_COMPILE_AND_EXECUTE);
                }
                gl.glPushMatrix();
                disableLight();
                gl.glColor4f(25.0f / 255.0f, 128.0f / 255.0f, 203.0f / 255.0f, 1.0f);
                glu.gluSphere(gluQuadric, radius, 15, 15);
                gl.glPopMatrix();
                if (useDisplayList) {
                    gl.glEndList();
                }
            } else {
                gl.glCallList(sphereList2);
            }

            if (sphereList3 == -1 || !useDisplayList) {
                if (useDisplayList) {
                    sphereList3 = gl.glGenLists(1);
                    gl.glNewList(sphereList3, gl.GL_COMPILE_AND_EXECUTE);
                }
                disableLight();
                gl.glPushMatrix();
                gl.glColorMask(false, false, false, false);
                gl.glColor4f(25.0f / 255.0f, 128.0f / 255.0f, 203.0f / 255.0f, 1.0f);
                gl.glScalef(1.75f, 1.75f, 1.75f);
                glu.gluSphere(gluQuadric, radius, 15, 15);
                gl.glColorMask(true, true, true, true);
                gl.glPopMatrix();
                if (useDisplayList) {
                    gl.glEndList();
                }
            } else {
                gl.glCallList(sphereList3);
            }
            gl.glPopMatrix();

            if (isRenderDebugInfo()) {
                disableLight();
                gl.glDepthMask(false);
                gl.glDisable(gl.GL_DEPTH_TEST);

                String text = "" + node.getNumEntities();
                renderBillboardedText(text, pos, true);

                gl.glDepthMask(true);
                gl.glEnable(gl.GL_DEPTH_TEST);
            }
        }
    }

    @Override
    public void renderStalk(nvStalk stalk) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);
        enableLight();
        float radius = simulator.getProjection().geoToVizApprox(0.5f);
        nvVec3 pos1 = stalk.getSource().getCurrentRenderPos();
        nvVec3 pos2 = stalk.getDestination().getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos1.x, pos1.y, pos1.z), radius) != nvFrustum.ALIGN.OUTSIDE ||
                frustum.sphereInFrustum(new nvVec3(pos2.x, pos2.y, pos2.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
            float width = 1.0f;
            gl.glLineWidth(width);
            //gl.glColor4f(24.0f/255.0f, 128.0f/255.0f, 203.0f/255.0f, 1.0f);
            gl.glColor4f(0.0f, 0.0f, 0.0f, 0.75f);
            gl.glBegin(gl.GL_LINES);
            gl.glVertex3f(pos1.x, pos1.y, pos1.z);
            gl.glVertex3f(pos2.x, pos2.y, pos2.z);
            gl.glEnd();
            gl.glLineWidth(1.0f);
        }
    }

    @Override
    public void renderLeaf(nvLeaf leaf) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        float radius = simulator.getProjection().geoToVizApprox(3.0f);
        nvVec3 pos = leaf.getCurrentRenderPos();
        if (frustum.sphereInFrustum(new nvVec3(pos.x, pos.y, pos.z), radius) != nvFrustum.ALIGN.OUTSIDE) {

            gl.glPushMatrix();
            gl.glTranslatef(pos.x, pos.y, pos.z);
            gl.glColor4f(leaf.getVizValue(VIZPARAM.APPERANCE_COLOR_R) / 2.0f, leaf.getVizValue(VIZPARAM.APPERANCE_COLOR_G) + 0.5f, leaf.getVizValue(VIZPARAM.APPERANCE_COLOR_B) / 2.0f, 1.0f);
            if (sphereList4 == -1) {
                sphereList4 = gl.glGenLists(1);
                gl.glNewList(sphereList4, gl.GL_COMPILE_AND_EXECUTE);
                //gl.glColor4f(25.0f/255.0f, 128.0f/255.0f, 203.0f/255.0f, 1.0f);
                enableLight();
                glu.gluSphere(gluQuadric, radius, 12, 12);
                gl.glEndList();
            } else {
                gl.glCallList(sphereList4);
            }
            gl.glPopMatrix();
        }
    }

    @Override
    public void renderDataEntity(nvDataEntity entity) {
        gl.glEnable(gl.GL_BLEND);
        gl.glEnable(gl.GL_DEPTH_TEST);

        enableLight();
        enableFog();

        if (isRenderDebugInfo()) {
            float radius = simulator.getProjection().geoToVizApprox(1.25f);
            nvVec3 pos = simulator.getProjection().geoToViz(entity.getPosition());
            if (frustum.sphereInFrustum(new nvVec3(pos.x, 0, pos.z), radius) != nvFrustum.ALIGN.OUTSIDE) {
                gl.glPushMatrix();
                gl.glTranslatef(pos.x, 0, pos.z);
                gl.glColor4f(1, 0, 0, 1.0f);
                glu.gluSphere(gluQuadric, radius, 5, 5);
                gl.glPopMatrix();
            }
        }

        // TODO implement?? (probably as part of the place rendering)

        disableLight();
        disableFog();
    }

/*
        // these attributes need to be set, when shadow is rendered!

        glDepthMask(GL_FALSE);
        glDisable(GL_CULL_FACE);

        // RENDERING

        glEnable(GL_CULL_FACE);
        glDepthMask(GL_TRUE);
*/
    private final void setShadowFloorPlane(nvVec3 v0, nvVec3 v1, nvVec3 v2) {
        // Need 2 vectors to find cross product
        nvVec3 vec0 = v1.sub(v0);
        nvVec3 vec1 = v2.sub(v0);

        // find cross product to get A, B, and C of plane equation
        shadowGroundplane[0] = vec0.y * vec1.z - vec0.z * vec1.y;
        shadowGroundplane[1] = -(vec0.x * vec1.z - vec0.z * vec1.x);
        shadowGroundplane[2] = vec0.x * vec1.y - vec0.y * vec1.x;
        shadowGroundplane[3] = -(shadowGroundplane[0] * v0.x + shadowGroundplane[1] * v0.y + shadowGroundplane[2] * v0.z);
        calculateShadowTransformMatrix();
    }

    private final void setShadowLightPos(float[] pos) {
        for (int i = 0; i < 4; i++) {
            shadowLightPosition[i] = pos[i];
        }
        calculateShadowTransformMatrix();
    }

    private final void calculateShadowTransformMatrix() {
        // Find dot  product  between  light position vector and ground plane normal.
        float dot = shadowGroundplane[0] * shadowLightPosition[0]
                + shadowGroundplane[1] * shadowLightPosition[1]
                + shadowGroundplane[2] * shadowLightPosition[2]
                + shadowGroundplane[3] * shadowLightPosition[3];

        shadowTransMat[0] = dot - shadowLightPosition[0] * shadowGroundplane[0];
        shadowTransMat[4] = 0.f - shadowLightPosition[0] * shadowGroundplane[1];
        shadowTransMat[8] = 0.f - shadowLightPosition[0] * shadowGroundplane[2];
        shadowTransMat[12] = 0.f - shadowLightPosition[0] * shadowGroundplane[3];

        shadowTransMat[1] = 0.f - shadowLightPosition[1] * shadowGroundplane[0];
        shadowTransMat[5] = dot - shadowLightPosition[1] * shadowGroundplane[1];
        shadowTransMat[9] = 0.f - shadowLightPosition[1] * shadowGroundplane[2];
        shadowTransMat[13] = 0.f - shadowLightPosition[1] * shadowGroundplane[3];

        shadowTransMat[2] = 0.f - shadowLightPosition[2] * shadowGroundplane[0];
        shadowTransMat[6] = 0.f - shadowLightPosition[2] * shadowGroundplane[1];
        shadowTransMat[10] = dot - shadowLightPosition[2] * shadowGroundplane[2];
        shadowTransMat[14] = 0.f - shadowLightPosition[2] * shadowGroundplane[3];

        shadowTransMat[3] = 0.f - shadowLightPosition[3] * shadowGroundplane[0];
        shadowTransMat[7] = 0.f - shadowLightPosition[3] * shadowGroundplane[1];
        shadowTransMat[11] = 0.f - shadowLightPosition[3] * shadowGroundplane[2];
        shadowTransMat[15] = dot - shadowLightPosition[3] * shadowGroundplane[3];
    }

    private final void beginStencilShadow2() {
        if (renderpass == SHADOW_RENDERPASS) {
            gl.glEnable(gl.GL_STENCIL_TEST);
            gl.glStencilFunc(gl.GL_ALWAYS, 3, 0xffffffff);
            gl.glStencilOp(gl.GL_KEEP, gl.GL_KEEP, gl.GL_REPLACE);

            gl.glDisable(gl.GL_LIGHTING);
            gl.glColorMask(false, false, false, false);
            gl.glBegin(gl.GL_QUADS);
            gl.glVertex3f(frustum.gnl.x, 0, frustum.gnl.z);
            gl.glVertex3f(frustum.gnr.x, 0, frustum.gnr.z);
            gl.glVertex3f(frustum.gfr.x, 0, frustum.gfr.z);
            gl.glVertex3f(frustum.gfl.x, 0, frustum.gfl.z);
            gl.glEnd();
            gl.glColorMask(true, true, true, true);
            gl.glEnable(gl.GL_LIGHTING);
        }
    }

    private final void endStencilShadow2(float opacity, float r, float g, float b) {
        if (renderpass == SHADOW_RENDERPASS) {

            /* Now, only render where stencil is set above 2 (ie, 3 where
            the top floor is).  Update stencil with 2 where the shadow
            gets drawn so we don't redraw (and accidently reblend) the
            shadow). */
            gl.glStencilFunc(gl.GL_LESS, 2, 0xffffffff);  /* draw if ==1 */
            gl.glStencilOp(gl.GL_REPLACE, gl.GL_REPLACE, gl.GL_REPLACE);

            /* To eliminate depth buffer artifacts, we use polygon offset
            to raise the depth of the projected shadow slightly so
            that it does not depth buffer alias with the floor. */
            gl.glEnable(gl.GL_POLYGON_OFFSET_FILL);

            /* Render 50% black shadow color on top of whatever the
            floor appareance is. */
            gl.glEnable(gl.GL_BLEND);
            gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA);
            gl.glDisable(gl.GL_LIGHTING);  /* Force the 50% black. */
            gl.glColor4f(r, g, b, opacity);
            gl.glPushMatrix();
            /* Project the shadow. */
            gl.glMultMatrixf(shadowTransMat, 0);

        } else {
            gl.glPopMatrix();
            gl.glEnable(gl.GL_LIGHTING);
            gl.glDisable(gl.GL_POLYGON_OFFSET_FILL);
            gl.glDisable(gl.GL_STENCIL_TEST);
        }
    }

    private final void beginStencilShadow() {
        if (renderpass == SHADOW_RENDERPASS) {
            gl.glDisable(gl.GL_LIGHTING);
            gl.glShadeModel(gl.GL_FLAT);
            gl.glEnable(gl.GL_DEPTH_TEST);

            gl.glPushMatrix();

            // set projection matrix
            gl.glMultMatrixf(shadowTransMat, 0);

            // Setup Stencil
            gl.glClearStencil(0);
            gl.glClear(gl.GL_STENCIL_BUFFER_BIT);
            gl.glStencilFunc(gl.GL_ALWAYS, 0x1, 0x1);
            gl.glStencilOp(gl.GL_KEEP, gl.GL_KEEP, gl.GL_REPLACE);
            gl.glEnable(gl.GL_STENCIL_TEST);
            gl.glEnable(gl.GL_DEPTH_TEST);

            gl.glColorMask(false, false, false, false);
            gl.glDepthMask(false);
        }
    }

    private final void endStencilShadow(float opacity, float r, float g, float b) {
        if (renderpass == SHADOW_RENDERPASS) {
            // now draw shadow according to stencil
            gl.glColorMask(true, true, true, true);
            gl.glDepthMask(false);

            //draw a shadowing rectangle covering the entire screen
            gl.glDisable(gl.GL_DEPTH_TEST);
            gl.glEnable(gl.GL_BLEND);
            gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA);
            gl.glColor4f(r, g, b, opacity);
            gl.glEnable(gl.GL_STENCIL_TEST);
            gl.glStencilFunc(gl.GL_EQUAL, 0x1, 0x1);
            gl.glStencilOp(gl.GL_KEEP, gl.GL_KEEP, gl.GL_KEEP);

            gl.glMatrixMode(gl.GL_PROJECTION);
            gl.glPushMatrix();
            {
                gl.glLoadIdentity();

                gl.glMatrixMode(gl.GL_MODELVIEW);
                gl.glPushMatrix();
                {
                    gl.glLoadIdentity();

                    gl.glBegin(gl.GL_QUADS);
                    gl.glVertex3f(-1.0f, -1.0f, -1.0f);
                    gl.glVertex3f(1.0f, -1.0f, -1.0f);
                    gl.glVertex3f(1.0f, 1.0f, -1.0f);
                    gl.glVertex3f(-1.0f, 1.0f, -1.0f);
                    gl.glEnd();
                }
                gl.glPopMatrix();

                gl.glMatrixMode(gl.GL_PROJECTION);
            }
            gl.glPopMatrix();

            // reset matrix et al
            gl.glMatrixMode(gl.GL_MODELVIEW);
            gl.glDisable(gl.GL_BLEND);
            gl.glEnable(gl.GL_DEPTH_TEST);
            gl.glDepthMask(true);
            gl.glEnable(gl.GL_LIGHTING);

            gl.glDisable(gl.GL_STENCIL_TEST);
            gl.glShadeModel(gl.GL_SMOOTH);
            gl.glDisable(gl.GL_BLEND);

            gl.glPopMatrix();
        }
    }

}